/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.activiti.camel;

import org.activiti.bpmn.model.MapExceptionEntry;
import org.activiti.engine.ActivitiException;
import org.activiti.engine.ProcessEngineConfiguration;
import org.activiti.engine.delegate.BpmnError;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.Expression;
import org.activiti.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.activiti.engine.impl.bpmn.helper.ErrorPropagation;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.pvm.PvmProcessDefinition;
import org.activiti.engine.impl.pvm.delegate.ActivityBehavior;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.apache.camel.CamelContext;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.commons.lang3.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This abstract class takes the place of the now-deprecated CamelBehaviour class (which can still be used for legacy compatibility)
 * and significantly improves on its flexibility. Additional implementations can be created that change the way in which Activiti
 * interacts with Camel per your specific needs.
 * <p>
 * Three out-of-the-box implementations of CamelBehavior are provided:
 * (1) CamelBehaviorDefaultImpl: Works just like CamelBehaviour does; copies variables into and out of Camel as or from properties.
 * (2) CamelBehaviorBodyAsMapImpl: Works by copying variables into and out of Camel using a Map<String,Object> object in the body.
 * (3) CamelBehaviorCamelBodyImpl: Works by copying a single variable value into Camel as a String body and copying the Camel
 * body into that same Activiti variable. The variable in Activiti must be named "camelBody".
 * <p>
 * The chosen implementation should be set within your ProcessEngineConfiguration. To specify the implementation using Spring, include
 * the following line in your configuration file as part of the properties for "org.activiti.spring.SpringProcessEngineConfiguration":
 *
 * <property name="camelBehaviorClass" value="org.activiti.camel.impl.CamelBehaviorCamelBodyImpl"/>
 * <p>
 * Note also that the manner in which variables are copied to Activiti from Camel has changed. It will always copy Camel
 * properties to the Activiti variable set; they can safely be ignored, of course, if not required. It will conditionally
 * copy the Camel body to the "camelBody" variable if it is of type java.lang.String, OR it will copy the Camel body to
 * individual variables within Activiti if it is of type Map<String,Object>.
 *
 * @author Ryan Johnston (@rjfsu), Tijs Rademakers, Saeid Mirzaei
 * @version 5.12
 */
public abstract class CamelBehavior extends AbstractBpmnActivityBehavior implements ActivityBehavior {

    private static final long serialVersionUID = 1L;
    protected Expression camelContext;
    protected CamelContext camelContextObj;
    protected SpringProcessEngineConfiguration springConfiguration;
    protected List<MapExceptionEntry> mapExceptions;
    protected TargetType toTargetType = null;

    protected abstract void setPropertTargetVariable(ActivitiEndpoint endpoint);

    protected void updateTargetVariables(ActivitiEndpoint endpoint) {
        toTargetType = null;
        if (endpoint.isCopyVariablesToBodyAsMap())
            toTargetType = TargetType.BODY_AS_MAP;
        else if (endpoint.isCopyCamelBodyToBody())
            toTargetType = TargetType.BODY;
        else if (endpoint.isCopyVariablesToProperties())
            toTargetType = TargetType.PROPERTIES;

        if (toTargetType == null)
            setPropertTargetVariable(endpoint);
    }

    protected void copyVariables(Map<String, Object> variables, Exchange exchange, ActivitiEndpoint endpoint) {
        switch (toTargetType) {
            case BODY_AS_MAP:
                copyVariablesToBodyAsMap(variables, exchange);
                break;

            case BODY:
                copyVariablesToBody(variables, exchange);
                break;

            case PROPERTIES:
                copyVariablesToProperties(variables, exchange);
        }
    }

    public void execute(ActivityExecution execution) throws Exception {
        setAppropriateCamelContext(execution);

        final ActivitiEndpoint endpoint = createEndpoint(execution);
        final Exchange exchange = createExchange(execution, endpoint);

        endpoint.process(exchange);
        execution.setVariables(ExchangeUtils.prepareVariables(exchange, endpoint));
        if (!handleCamelException(exchange, execution))
            leave(execution);
    }

    protected ActivitiEndpoint createEndpoint(ActivityExecution execution) {
        String uri = "activiti://" + getProcessDefinitionKey(execution) + ":" + execution.getActivity().getId();
        return getEndpoint(uri);
    }

    protected ActivitiEndpoint getEndpoint(String key) {
        for (Endpoint e : camelContextObj.getEndpoints()) {
            if (e.getEndpointKey().equals(key) && (e instanceof ActivitiEndpoint)) {
                return (ActivitiEndpoint) e;
            }
        }
        throw new ActivitiException("Activiti endpoint not defined for " + key);
    }

    protected Exchange createExchange(ActivityExecution activityExecution, ActivitiEndpoint endpoint) {
        Exchange ex = endpoint.createExchange();
        ex.setProperty(ActivitiProducer.PROCESS_ID_PROPERTY, activityExecution.getProcessInstanceId());
        ex.setProperty(ActivitiProducer.EXECUTION_ID_PROPERTY, activityExecution.getId());
        Map<String, Object> variables = activityExecution.getVariables();
        updateTargetVariables(endpoint);
        copyVariables(variables, ex, endpoint);
        return ex;
    }

    protected boolean handleCamelException(Exchange exchange, ActivityExecution execution) throws Exception {
        Exception camelException = exchange.getException();
        boolean notHandledByCamel = exchange.isFailed() && camelException != null;
        if (notHandledByCamel) {
            if (camelException instanceof BpmnError) {
                ErrorPropagation.propagateError((BpmnError) camelException,
                        execution);
                return true;
            } else {
                if (ErrorPropagation.mapException(camelException, execution, mapExceptions))
                    return true;
                else
                    throw new ActivitiException("Unhandled exception on camel route", camelException);
            }
        }
        return false;
    }

    protected void copyVariablesToProperties(Map<String, Object> variables, Exchange exchange) {
        for (Map.Entry<String, Object> var : variables.entrySet()) {
            exchange.setProperty(var.getKey(), var.getValue());
        }
    }

    protected void copyVariablesToBodyAsMap(Map<String, Object> variables, Exchange exchange) {
        exchange.getIn().setBody(new HashMap<String, Object>(variables));
    }

    protected void copyVariablesToBody(Map<String, Object> variables, Exchange exchange) {
        Object camelBody = variables.get(ExchangeUtils.CAMELBODY);
        if (camelBody != null) {
            exchange.getIn().setBody(camelBody);
        }
    }

    protected String getProcessDefinitionKey(ActivityExecution execution) {
        PvmProcessDefinition processDefinition = execution.getActivity().getProcessDefinition();
        return processDefinition.getKey();
    }

    protected boolean isASync(ActivityExecution execution) {
        return execution.getActivity().isAsync();
    }

    protected void setAppropriateCamelContext(ActivityExecution execution) {
        //Check to see if the springConfiguration has been set. If not, set it.
        if (springConfiguration == null) {
            //Get the ProcessEngineConfiguration object.
            ProcessEngineConfiguration engineConfiguration = Context.getProcessEngineConfiguration();

            //Convert it to a SpringProcessEngineConfiguration. If this doesn't work, throw a RuntimeException.
            // (ActivitiException extends RuntimeException.)
            try {
                springConfiguration = (SpringProcessEngineConfiguration) engineConfiguration;
            } catch (Exception e) {
                throw new ActivitiException("Expecting a SpringProcessEngineConfiguration for the Activiti Camel module.", e);
            }
        }

        //Get the appropriate String representation of the CamelContext object from ActivityExecution (if available).
        String camelContextValue = getStringFromField(camelContext, execution);

        //If the String representation of the CamelContext object from ActivityExecution is empty, use the default.
        if (StringUtils.isEmpty(camelContextValue) && camelContextObj != null) {
            //No processing required. No custom CamelContext & the default is already set.
        } else {
            if (StringUtils.isEmpty(camelContextValue) && camelContextObj == null) {
                camelContextValue = springConfiguration.getDefaultCamelContext();
            }

            //Get the CamelContext object and set the super's member variable.
            Object ctx = springConfiguration.getApplicationContext().getBean(camelContextValue);
            if (ctx == null || ctx instanceof CamelContext == false) {
                throw new ActivitiException("Could not find CamelContext named " + camelContextValue + ".");
            }
            camelContextObj = (CamelContext) ctx;
        }
    }

    protected String getStringFromField(Expression expression, DelegateExecution execution) {
        if (expression != null) {
            Object value = expression.getValue(execution);
            if (value != null) {
                return value.toString();
            }
        }
        return null;
    }

    public void setCamelContext(Expression camelContext) {
        this.camelContext = camelContext;
    }

    public enum TargetType {
        BODY_AS_MAP, BODY, PROPERTIES
    }
}
